Docly Developers
Knowledge base Best practice Recommended

Put configuration and secrets in the Config document

In short#/Config is the standard location for site configuration in Docly: a schema-bound document in the #/ tree, which is never served over HTTP, is editable through a generated form, and is readable from every template and API function. It is two pieces - a schema at #/Schemas/<Name> config.docly defining the fields, and #/Config.docly bound to it. Secrets belong here and nowhere else; use Type "password" so they are masked while editing. Public values such as a Turnstile site key belong here too, so the customer changes everything in one place. Read it with docly.getJson("#/Config"), guarded with || {} because it returns null before the document exists. Some examples use a config global instead - that was undefined in testing while getJson worked, and an unresolved name in a hash expression renders empty rather than failing, so verify before relying on it.
Applies to ConfigurationSecurity
  1. What you'll see

    A site needs a value that is not content and not code: an API key, a Turnstile secret, a tracking id, a feed endpoint, a per-environment toggle. It has to be readable by templates and API functions, editable without a deploy, and — for the secret half of it — unreachable from the web. The tempting move is a JSON file somewhere convenient, or a constant in a .hash file. Both are wrong, and the second one publishes the secret.

  2. What's actually happening

    #/Config is the standard location for site configuration in Docly. It is a schema-bound document in the #/ tree, which means three things at once: it is never served over HTTP, it is editable through a generated form rather than by hand-editing JSON, and its fields are readable from every template and API function.

    This is not one option among several. A workspace that scatters configuration across ad-hoc JSON files has no single place to look, no form to edit it in, and no guarantee about what is exposed.

    It is a document with a schema, not a loose file

    Two pieces: a schema at #/Schemas/<Name> config.docly defining the fields, and the document itself at #/Config.docly bound to that schema. The schema gives the customer a labelled form with descriptions, marks the required fields, and can use Type: "password" so a secret is not displayed in plain text while editing.

    Secrets belong here, and only here

    Anything under #/ is unreachable over HTTP — see Keep private data under the # folder. That is what makes #/Config the right home for a secret key, and it is why a secret must never be written into a .hash page, a stylesheet, or a JSON file in a published folder.

    Public values live here too. A Turnstile site key is public by design and is written into the markup — but it still belongs in #/Config, so the customer changes both keys in one place rather than hunting through templates.

    Reading it

    Read the document with docly.getJson("#/Config"). This works in both hash templates and API functions and returns the fields as a plain object.

    Some documentation and examples use a config global — #config.CF_Turnstile_Site_Key#. Verified 2026-08-26: that global was undefined in a hash template while docly.getJson("#/Config") returned the fields correctly on the same request. Use getJson, or verify the global with a scratch page before relying on it — an unresolved name in a hash expression renders empty rather than failing, so a wrong guess here silently produces a form with no site key.

  3. What to do

    1. The schema — #/Schemas/<Site> config.docly

    Ordinary Input fields. Use Type: "password" for secrets and put a real description on each one — the customer will be the one editing this.

    {
      "ObjectVariant": "Input",
      "Required": true,
      "Type": "password",
      "Name": "CF_Turnstile_Secret_Key",
      "Field": "Cloudflare Turnstile - secret",
      "Description": "Secret. Read only by #/API/Turnstile.js. Never goes in markup."
    }

    Name is the data key you read in code; Field is the label the customer sees. Not the other way round.

    2. The document — #/Config.docly

    {
      "Schema": "<Site> config",
      "Document": {
        "CF_Turnstile_Site_Key":   "0x4AAAAAAA...",
        "CF_Turnstile_Secret_Key": "0x4AAAAAAA..."
      },
      "Tags": [],
      "EmbeddedFiles": []
    }

    3. Reading it

    // In a hash template — public values only
    #{ var cfg = docly.getJson("#/Config") || {}; }#
    <div class="cf-turnstile" data-sitekey="#docly.htmlAttributeEncode(cfg.CF_Turnstile_Site_Key)#"></div>
    
    // In an API function — where the secret is used
    var cfg = docly.getJson("#/Config") || {};
    var secret = cfg.CF_Turnstile_Secret_Key;

    Always guard with || {}. Before the document exists, getJson returns null, and property access on it throws in an API function and renders empty in a template.

    Verify it is not served

    curl -s -o /dev/null -w "%{http_code}\n" https://<site>/Config

    Must return 404. Run it after creating the document, not from memory.

    Checklist

    1. Configuration lives in #/Config — not in ad-hoc JSON files, not as constants in templates.
    2. A schema backs it, so the customer gets a form with labels and descriptions.
    3. Secrets use Type: "password" and are read only where they are used.
    4. Read with docly.getJson("#/Config"), guarded with || {}.
    5. /Config returns 404.

Last reviewed 26 Aug 2026